﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RayDen.Library.Components.Surface;
using RayDen.Library.Components.SystemComponents;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Types;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;

namespace RayDen.RayEngine.Engines.RayTracer
{

    [Flags]
    public enum RayTracerPathState : byte
    {
        Eye = 0,
        Reflection = 1,
        Refraction = 2,
        OnlyShadow = 4
    }

    public class RayTracerPathSampler : PathSamplerBase
    {
        private RgbSpectrum Ambient = new RgbSpectrum(0.0f);
        private bool finished;
        private RayTracerPathState State;

        protected RayEngineScene scene;
        protected RgbSpectrum Throughput;

        private ShadowRayInfo[] secRays;
        protected int depth, tracedShadowRayCount;
        protected float pathWeight;
        protected bool specularBounce;
        SurfaceIntersectionData hitInfo = null;

        public RayTracerPathSampler[] Children;

        public override void InitPath(IPathProcessor buffer)
        {
            base.InitPath(buffer);
            this.scene = pathIntegrator.Scene;

            if (this.secRays == null)
            {
                this.secRays = new ShadowRayInfo[scene.ShadowRaysPerSample];
            }

            if (State == RayTracerPathState.Eye || State == RayTracerPathState.OnlyShadow || depth >= scene.MaxPathDepth)
            {
                this.Sample = pathIntegrator.Sampler.GetSample(this.Sample);
                pathIntegrator.Scene.Camera.GetRay(Sample.imageX, Sample.imageY, out this.PathRay);
                this.RayIndex = -1;
                this.pathWeight = 1.0f;
                this.tracedShadowRayCount = 0;
                this.depth = 0;
                this.Throughput = new RgbSpectrum(1f);
                this.Radiance = Ambient;
                this.specularBounce = true;
                this.State = RayTracerPathState.Eye;
                this.finished = false;
            }
            else if (State == RayTracerPathState.Reflection)
            {
                this.RayIndex = -1;
                this.pathWeight = 1.0f;
                this.tracedShadowRayCount = 0;
                this.specularBounce = true;
            }
            else if (State == RayTracerPathState.Refraction)
            {
                this.RayIndex = -1;
                this.pathWeight = 1.0f;
                this.tracedShadowRayCount = 0;
                this.specularBounce = true;
            }
        }

        public override bool FillRayBuffer(RayBuffer rayBuffer)
        {
            var leftSpace = rayBuffer.LeftSpace();
            if (((State == RayTracerPathState.Eye) && (1 > leftSpace)) ||
            ((State == RayTracerPathState.OnlyShadow) && (tracedShadowRayCount > leftSpace)) ||
            ((State == RayTracerPathState.Reflection || State == RayTracerPathState.Refraction) && (tracedShadowRayCount + 1 > leftSpace)))
                return false;
            if (State != RayTracerPathState.OnlyShadow)
                RayIndex = rayBuffer.AddRay(ref PathRay);
            if (State == RayTracerPathState.Reflection || State == RayTracerPathState.Refraction || State == RayTracerPathState.OnlyShadow)
            {
                for (int i = 0; i < tracedShadowRayCount; ++i)
                    secRays[i].ShadowRayIndex = rayBuffer.AddRay(ref secRays[i].ShadowRay);
            }
            return true;
        }


        public override void Advance(RayBuffer rayBuffer, SampleBuffer consumer)
        {
            int currentTriangleIndex = 0;
#if VERBOSE

            try
            {
#endif

                if ((State != RayTracerPathState.Eye) && (tracedShadowRayCount > 0))
                {
                    for (int i = 0; i < tracedShadowRayCount; ++i)
                    {
                        RayHit shadowRayHit = rayBuffer.rayHits[secRays[i].ShadowRayIndex];
                        RgbSpectrum attenuation;
                        bool continueTrace;
                        if (this.ShadowRayTest(ref shadowRayHit, ref secRays[i].ShadowRay, out attenuation,
                            out continueTrace))
                        {
                            Radiance += attenuation*((secRays[i].Throughput)/secRays[i].Pdf);
                            pathWeight *= secRays[i].Pdf;
                        }


                    }

                    if (State == RayTracerPathState.OnlyShadow)
                    {
                        Splat(consumer);
                        return;
                    }

                    tracedShadowRayCount = 0;
                }

                var rayHit = rayBuffer.rayHits[RayIndex];
                var wo = -PathRay.Dir;

                depth++;
                var missed = rayHit.Index == 0xffffffffu;
                if (missed || State == RayTracerPathState.OnlyShadow || depth > scene.MaxPathDepth)
                {
                    if (missed)
                    {
                        //Radiance += this.scene.SampleEnvironment(ref wo) * Throughput;
                        var sampledEnvironment = this.scene.SampleEnvironment(ref wo);
                        Radiance.MAdd(ref sampledEnvironment, ref Throughput);
                    }
                    Splat(consumer);
                    return;
                }

                if (hitInfo == null)
                {
                    hitInfo = SurfaceSampler.GetIntersection(ref PathRay, ref rayHit);
                }
                else
                {
                    SurfaceSampler.GetIntersection(ref PathRay, ref rayHit, ref hitInfo);
                }

                currentTriangleIndex = (int) rayHit.Index;
                var hitPoint = PathRay.Point(rayHit.Distance);

                tracedShadowRayCount = 0;
                var bsdf = hitInfo.MMaterial;
                if (bsdf.IsDiffuse())
                {
                    float lightStrategyPdf = LightSampler.StrategyPdf;
                    //scene.ShadowRaysPerSample/(float)scene.Lights.Length;
                    RgbSpectrum lightTroughtput = Throughput*hitInfo.Color;
                    LightSampler.EvaluateShadow(ref hitPoint, ref hitInfo.Normal, Sample.GetLazyValue(),
                        Sample.GetLazyValue(), Sample.GetLazyValue(), ref ls);
                    for (int index = 0; index < ls.Length; index++)
                    {

                        if (ls[index].Pdf <= 0f)
                            continue;
                        secRays[tracedShadowRayCount].Throughput = ls[index].Spectrum.GetType() ==
                                                                   typeof (RgbSpectrumInfo)
                            ? (RgbSpectrum) (RgbSpectrumInfo) ls[index].Spectrum
                            : (RgbSpectrum) ls[index].Spectrum;
                        secRays[tracedShadowRayCount].Pdf = ls[index].Pdf;
                        secRays[tracedShadowRayCount].ShadowRay = ls[index].LightRay;
                        Vector lwi = secRays[tracedShadowRayCount].ShadowRay.Dir;
                        RgbSpectrum fs;
                        hitInfo.MMaterial.f(ref secRays[tracedShadowRayCount].ShadowRay.Dir, ref wo,
                            ref hitInfo.ShadingNormal, ref Throughput, out fs, types: BrdfType.Diffuse);
                        secRays[tracedShadowRayCount].Throughput *= lightTroughtput*
                                                                    Vector.AbsDot(ref hitInfo.Normal, ref lwi)*
                                                                    fs;
                        if (!secRays[tracedShadowRayCount].Throughput.IsBlack())
                        {
#if DEBUG
                            RayDen.Library.Core.Components.Assert.IsNotNaN(secRays[tracedShadowRayCount].Pdf);
#endif
                            secRays[tracedShadowRayCount].Pdf /= lightStrategyPdf;
                            tracedShadowRayCount++;
                        }
                    }
                }

                if (bsdf.IsSpecular() || bsdf.IsRefractive() && depth < scene.MaxPathDepth)
                {
                    var fPdf = 0f;
                    var wi = new Vector();
                    RgbSpectrum f;
                    hitInfo.TextureData.Throughput = Throughput;
                    f = bsdf.Sample_f(ref wo, out wi, ref hitInfo.Normal, ref hitInfo.ShadingNormal, ref Throughput,
                        Sample.GetLazyValue(), Sample.GetLazyValue(), Sample.GetLazyValue()
                        , ref hitInfo.TextureData, out fPdf, out specularBounce);
                    if ((fPdf <= 0.0f) || f.IsBlack())
                    {
                        if (tracedShadowRayCount > 0)
                            State = RayTracerPathState.OnlyShadow;
                        else
                        {
                            Splat(consumer);

                        }
                        return;
                    }
                    pathWeight *= fPdf;
                    Throughput *= (f)/fPdf;
                    State = RayTracerPathState.Reflection;
                    PathRay.Org = hitPoint;
                    PathRay.Dir = wi.Normalize();
                    //Radiance += Throughput;
                }
                else
                {
                    if (tracedShadowRayCount > 0)
                        State = RayTracerPathState.OnlyShadow;
                    else
                        Splat(consumer);
                }
#if VERBOSE
            }
            catch (Exception ex)
            {
                Tracer.TraceLine(ex.Message);
                throw;
            }

#endif
        }

        public override void Splat(SampleBuffer sampleBuffer)
        {
#if VERBOSE
            this.pathIntegrator.splattedCount++;
#endif

            if (State == RayTracerPathState.OnlyShadow || State == RayTracerPathState.Eye || depth >= scene.MaxPathDepth)
            {
                if (Radiance.IsBlack() && Throughput.Average < 0.99f)
                {
                    Radiance += Throughput * hitInfo.Color;
                }
                sampleBuffer.SplatSample(this.Sample.imageX, this.Sample.imageY, ref this.Radiance);
            }
            this.InitPath(pathIntegrator);
        }


    }
}
